--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Node / ReticulumProjects / MeshChatX.git / files / tests / backend / test_legacy_migration_sql_safety.py
Displaying Raw • Download
tests/backend/test_legacy_migration_sql_safety.py b1b9846ac58289ad2ac7f6564863fc97dd720128 (b1b9846a) Text, 13.42 KB
T8b949e"""Legacy migration and schema: safe ATTACH paths, identifiers, and raw SQL helpers.
Covers ATTACH DATABASE path escaping in LegacyMigrator, column identifier filtering,
and DatabaseSchema _validate_identifier / _ensure_column behaviour.
"""
Tff7b72import T7ee787os
Tff7b72import T7ee787re
Tff7b72import T7ee787sqlite3
Tff7b72import T7ee787pytest
Tff7b72from T7ee787hypothesis Tff7b72import Te6edf3HealthCheckTb4b4b4, Te6edf3givenTb4b4b4, Te6edf3settings
Tff7b72from T7ee787hypothesis Tff7b72import Te6edf3strategies Tff7b72as Te6edf3st
Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787backendT7ee787.T7ee787databaseT7ee787.T7ee787legacy_migrator Tff7b72import Te6edf3LegacyMigrator
Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787backendT7ee787.T7ee787databaseT7ee787.T7ee787provider Tff7b72import Te6edf3DatabaseProvider
Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787backendT7ee787.T7ee787databaseT7ee787.T7ee787schema Tff7b72import Te6edf3DatabaseSchemaTb4b4b4, Te6edf3_validate_identifier
T8b949e# ---------------------------------------------------------------------------
T8b949e# Fixtures
T8b949e# ---------------------------------------------------------------------------
Tf0883e@pytestTff7b72.Td2a8fffixture
Tff7b72def Td2a8ffdb_envTb4b4b4(Te6edf3tmp_pathTb4b4b4)Tb4b4b4:
T8b949e"""Provide an initialized DatabaseProvider + Schema in a temp directory."""
Te6edf3db_path Tff7b72= Tffa657strTb4b4b4(Te6edf3tmp_path Tff7b72/ Ta5d6ff"Ta5d6ffcurrent.dbTa5d6ff"Tb4b4b4)
Te6edf3provider Tff7b72= Te6edf3DatabaseProviderTb4b4b4(Te6edf3db_pathTb4b4b4)
Te6edf3schema Tff7b72= Te6edf3DatabaseSchemaTb4b4b4(Te6edf3providerTb4b4b4)
Te6edf3schemaTff7b72.Td2a8ffinitializeTb4b4b4(Tb4b4b4)
Tff7b72yield Te6edf3providerTb4b4b4, Te6edf3schemaTb4b4b4, Te6edf3tmp_path
Te6edf3providerTff7b72.Td2a8ffcloseTb4b4b4(Tb4b4b4)
Tff7b72def Td2a8ff_make_legacy_dbTb4b4b4(Te6edf3legacy_dirTb4b4b4, Te6edf3identity_hashTb4b4b4, Te6edf3tables_sqlTb4b4b4)Tb4b4b4:
T8b949e"""Create a legacy database with the given CREATE TABLE + INSERT statements."""
Te6edf3identity_dir Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3legacy_dirTb4b4b4, Ta5d6ff"Ta5d6ffidentitiesTa5d6ff"Tb4b4b4, Te6edf3identity_hashTb4b4b4)
Te6edf3osTff7b72.Td2a8ffmakedirsTb4b4b4(Te6edf3identity_dirTb4b4b4, Te6edf3exist_okTff7b72=Tff7b72TrueTb4b4b4)
Te6edf3db_path Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3identity_dirTb4b4b4, Ta5d6ff"Ta5d6ffdatabase.dbTa5d6ff"Tb4b4b4)
Te6edf3conn Tff7b72= Te6edf3sqlite3Tff7b72.Td2a8ffconnectTb4b4b4(Te6edf3db_pathTb4b4b4)
Tff7b72for Te6edf3sql Tff7b72in Te6edf3tables_sqlTb4b4b4:
Te6edf3connTff7b72.Td2a8ffexecuteTb4b4b4(Te6edf3sqlTb4b4b4)
Te6edf3connTff7b72.Td2a8ffcommitTb4b4b4(Tb4b4b4)
Te6edf3connTff7b72.Td2a8ffcloseTb4b4b4(Tb4b4b4)
Tff7b72return Te6edf3db_path
T8b949e# ---------------------------------------------------------------------------
T8b949e# 1. ATTACH DATABASE — single-quote escaping
T8b949e# ---------------------------------------------------------------------------
Tff7b72class T56d364TestAttachDatabasePathEscapingTb4b4b4:
Tff7b72def Td2a8fftest_path_without_quotes_migrates_normallyTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3db_envTb4b4b4)Tb4b4b4:
Te6edf3providerTb4b4b4, Te6edf3_schemaTb4b4b4, Te6edf3tmp_path Tff7b72= Te6edf3db_env
Te6edf3legacy_dir Tff7b72= Tffa657strTb4b4b4(Te6edf3tmp_path Tff7b72/ Ta5d6ff"Ta5d6fflegacy_normalTa5d6ff"Tb4b4b4)
Te6edf3identity_hash Tff7b72= Ta5d6ff"Ta5d6ffaabbccddTa5d6ff"
Te6edf3_make_legacy_dbTb4b4b4(
Te6edf3legacy_dirTb4b4b4,
Te6edf3identity_hashTb4b4b4,
Tb4b4b4[
Ta5d6ff"Ta5d6ffCREATE TABLE config (key TEXT UNIQUE, value TEXT)Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffINSERT INTO config (key, value) VALUES (Ta5d6ff'Ta5d6ffk1Ta5d6ff'Ta5d6ff, Ta5d6ff'Ta5d6ffv1Ta5d6ff'Ta5d6ff)Ta5d6ff"Tb4b4b4,
Tb4b4b4]Tb4b4b4,
Tb4b4b4)
Te6edf3migrator Tff7b72= Te6edf3LegacyMigratorTb4b4b4(Te6edf3providerTb4b4b4, Te6edf3legacy_dirTb4b4b4, Te6edf3identity_hashTb4b4b4)
Tff7b72assert Te6edf3migratorTff7b72.Td2a8ffmigrateTb4b4b4(Tb4b4b4) Tff7b72is Tff7b72True
Te6edf3row Tff7b72= Te6edf3providerTff7b72.Td2a8fffetchoneTb4b4b4(Ta5d6ff"Ta5d6ffSELECT value FROM config WHERE key = Ta5d6ff'Ta5d6ffk1Ta5d6ff'Ta5d6ff"Tb4b4b4)
Tff7b72assert Te6edf3row Tff7b72is Tff7b72not Tff7b72None
Tff7b72assert Te6edf3rowTb4b4b4[Ta5d6ff"Ta5d6ffvalueTa5d6ff"Tb4b4b4] Tff7b72== Ta5d6ff"Ta5d6ffv1Ta5d6ff"
Tff7b72def Td2a8fftest_path_with_single_quote_does_not_crashTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3db_envTb4b4b4)Tb4b4b4:
T8b949e"""A path containing a single quote must not cause SQL injection or crash."""
Te6edf3providerTb4b4b4, Te6edf3_schemaTb4b4b4, Te6edf3tmp_path Tff7b72= Te6edf3db_env
Te6edf3quoted_dir Tff7b72= Te6edf3tmp_path Tff7b72/ Ta5d6ff"Ta5d6ffitTa5d6ff'Ta5d6ffs_a_testTa5d6ff"
Te6edf3quoted_dirTff7b72.Td2a8ffmkdirTb4b4b4(Te6edf3parentsTff7b72=Tff7b72TrueTb4b4b4, Te6edf3exist_okTff7b72=Tff7b72TrueTb4b4b4)
Te6edf3legacy_dir Tff7b72= Tffa657strTb4b4b4(Te6edf3quoted_dirTb4b4b4)
Te6edf3identity_hash Tff7b72= Ta5d6ff"Ta5d6ffaabbccddTa5d6ff"
Te6edf3_make_legacy_dbTb4b4b4(
Te6edf3legacy_dirTb4b4b4,
Te6edf3identity_hashTb4b4b4,
Tb4b4b4[
Ta5d6ff"Ta5d6ffCREATE TABLE config (key TEXT UNIQUE, value TEXT)Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffINSERT INTO config (key, value) VALUES (Ta5d6ff'Ta5d6ffq1Ta5d6ff'Ta5d6ff, Ta5d6ff'Ta5d6ffquoted_valTa5d6ff'Ta5d6ff)Ta5d6ff"Tb4b4b4,
Tb4b4b4]Tb4b4b4,
Tb4b4b4)
Te6edf3migrator Tff7b72= Te6edf3LegacyMigratorTb4b4b4(Te6edf3providerTb4b4b4, Te6edf3legacy_dirTb4b4b4, Te6edf3identity_hashTb4b4b4)
Te6edf3result Tff7b72= Te6edf3migratorTff7b72.Td2a8ffmigrateTb4b4b4(Tb4b4b4)
Tff7b72assert Te6edf3result Tff7b72is Tff7b72True
Te6edf3row Tff7b72= Te6edf3providerTff7b72.Td2a8fffetchoneTb4b4b4(Ta5d6ff"Ta5d6ffSELECT value FROM config WHERE key = Ta5d6ff'Ta5d6ffq1Ta5d6ff'Ta5d6ff"Tb4b4b4)
Tff7b72assert Te6edf3row Tff7b72is Tff7b72not Tff7b72None
Tff7b72assert Te6edf3rowTb4b4b4[Ta5d6ff"Ta5d6ffvalueTa5d6ff"Tb4b4b4] Tff7b72== Ta5d6ff"Ta5d6ffquoted_valTa5d6ff"
Tff7b72def Td2a8fftest_path_with_multiple_quotesTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3db_envTb4b4b4)Tb4b4b4:
T8b949e"""Multiple single quotes in the path are all escaped."""
Te6edf3providerTb4b4b4, Te6edf3_schemaTb4b4b4, Te6edf3tmp_path Tff7b72= Te6edf3db_env
Te6edf3weird_dir Tff7b72= Te6edf3tmp_path Tff7b72/ Ta5d6ff"Ta5d6ffaTa5d6ff'Ta5d6ffbTa5d6ff'Ta5d6ffcTa5d6ff"
Te6edf3weird_dirTff7b72.Td2a8ffmkdirTb4b4b4(Te6edf3parentsTff7b72=Tff7b72TrueTb4b4b4, Te6edf3exist_okTff7b72=Tff7b72TrueTb4b4b4)
Te6edf3legacy_dir Tff7b72= Tffa657strTb4b4b4(Te6edf3weird_dirTb4b4b4)
Te6edf3identity_hash Tff7b72= Ta5d6ff"Ta5d6ff11223344Ta5d6ff"
Te6edf3_make_legacy_dbTb4b4b4(
Te6edf3legacy_dirTb4b4b4,
Te6edf3identity_hashTb4b4b4,
Tb4b4b4[
Ta5d6ff"Ta5d6ffCREATE TABLE config (key TEXT UNIQUE, value TEXT)Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffINSERT INTO config (key, value) VALUES (Ta5d6ff'Ta5d6ffmqTa5d6ff'Ta5d6ff, Ta5d6ff'Ta5d6ffmulti_quoteTa5d6ff'Ta5d6ff)Ta5d6ff"Tb4b4b4,
Tb4b4b4]Tb4b4b4,
Tb4b4b4)
Te6edf3migrator Tff7b72= Te6edf3LegacyMigratorTb4b4b4(Te6edf3providerTb4b4b4, Te6edf3legacy_dirTb4b4b4, Te6edf3identity_hashTb4b4b4)
Tff7b72assert Te6edf3migratorTff7b72.Td2a8ffmigrateTb4b4b4(Tb4b4b4) Tff7b72is Tff7b72True
Te6edf3row Tff7b72= Te6edf3providerTff7b72.Td2a8fffetchoneTb4b4b4(Ta5d6ff"Ta5d6ffSELECT value FROM config WHERE key = Ta5d6ff'Ta5d6ffmqTa5d6ff'Ta5d6ff"Tb4b4b4)
Tff7b72assert Te6edf3row Tff7b72is Tff7b72not Tff7b72None
Tff7b72assert Te6edf3rowTb4b4b4[Ta5d6ff"Ta5d6ffvalueTa5d6ff"Tb4b4b4] Tff7b72== Ta5d6ff"Ta5d6ffmulti_quoteTa5d6ff"
Tff7b72def Td2a8fftest_path_with_sql_injection_attemptTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3db_envTb4b4b4)Tb4b4b4:
T8b949e"""A path crafted to look like SQL injection is safely escaped."""
Te6edf3providerTb4b4b4, Te6edf3_schemaTb4b4b4, Te6edf3tmp_path Tff7b72= Te6edf3db_env
Te6edf3evil_dir Tff7b72= Te6edf3tmp_path Tff7b72/ Ta5d6ff"Ta5d6ff'Ta5d6ff; DROP TABLE config; --Ta5d6ff"
Te6edf3evil_dirTff7b72.Td2a8ffmkdirTb4b4b4(Te6edf3parentsTff7b72=Tff7b72TrueTb4b4b4, Te6edf3exist_okTff7b72=Tff7b72TrueTb4b4b4)
Te6edf3legacy_dir Tff7b72= Tffa657strTb4b4b4(Te6edf3evil_dirTb4b4b4)
Te6edf3identity_hash Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefTa5d6ff"
Te6edf3_make_legacy_dbTb4b4b4(
Te6edf3legacy_dirTb4b4b4,
Te6edf3identity_hashTb4b4b4,
Tb4b4b4[
Ta5d6ff"Ta5d6ffCREATE TABLE config (key TEXT UNIQUE, value TEXT)Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffINSERT INTO config (key, value) VALUES (Ta5d6ff'Ta5d6ffevilTa5d6ff'Ta5d6ff, Ta5d6ff'Ta5d6ffnopeTa5d6ff'Ta5d6ff)Ta5d6ff"Tb4b4b4,
Tb4b4b4]Tb4b4b4,
Tb4b4b4)
Te6edf3migrator Tff7b72= Te6edf3LegacyMigratorTb4b4b4(Te6edf3providerTb4b4b4, Te6edf3legacy_dirTb4b4b4, Te6edf3identity_hashTb4b4b4)
Te6edf3migratorTff7b72.Td2a8ffmigrateTb4b4b4(Tb4b4b4)
Te6edf3tables Tff7b72= Te6edf3providerTff7b72.Td2a8fffetchallTb4b4b4(
Ta5d6ff"Ta5d6ffSELECT name FROM sqlite_master WHERE type=Ta5d6ff'Ta5d6fftableTa5d6ff'Ta5d6ff AND name=Ta5d6ff'Ta5d6ffconfigTa5d6ff'Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Tff7b72assert Tffa657lenTb4b4b4(Te6edf3tablesTb4b4b4) Tff7b72> T79c0ff0Tb4b4b4, Ta5d6ff"Ta5d6ffconfig table must still exist after injection attemptTa5d6ff"
T8b949e# ---------------------------------------------------------------------------
T8b949e# 2. Legacy migrator — malicious column names filtered out
T8b949e# ---------------------------------------------------------------------------
Tff7b72class T56d364TestLegacyColumnFilteringTb4b4b4:
Tff7b72def Td2a8fftest_normal_columns_migrateTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3db_envTb4b4b4)Tb4b4b4:
T8b949e"""Standard column names pass through the identifier filter."""
Te6edf3providerTb4b4b4, Te6edf3_schemaTb4b4b4, Te6edf3tmp_path Tff7b72= Te6edf3db_env
Te6edf3legacy_dir Tff7b72= Tffa657strTb4b4b4(Te6edf3tmp_path Tff7b72/ Ta5d6ff"Ta5d6fflegacy_colsTa5d6ff"Tb4b4b4)
Te6edf3identity_hash Tff7b72= Ta5d6ff"Ta5d6ffaabb0011Ta5d6ff"
Te6edf3_make_legacy_dbTb4b4b4(
Te6edf3legacy_dirTb4b4b4,
Te6edf3identity_hashTb4b4b4,
Tb4b4b4[
Ta5d6ff"Ta5d6ffCREATE TABLE config (key TEXT UNIQUE, value TEXT)Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffINSERT INTO config (key, value) VALUES (Ta5d6ff'Ta5d6ffc1Ta5d6ff'Ta5d6ff, Ta5d6ff'Ta5d6ffokTa5d6ff'Ta5d6ff)Ta5d6ff"Tb4b4b4,
Tb4b4b4]Tb4b4b4,
Tb4b4b4)
Te6edf3migrator Tff7b72= Te6edf3LegacyMigratorTb4b4b4(Te6edf3providerTb4b4b4, Te6edf3legacy_dirTb4b4b4, Te6edf3identity_hashTb4b4b4)
Tff7b72assert Te6edf3migratorTff7b72.Td2a8ffmigrateTb4b4b4(Tb4b4b4) Tff7b72is Tff7b72True
Te6edf3row Tff7b72= Te6edf3providerTff7b72.Td2a8fffetchoneTb4b4b4(Ta5d6ff"Ta5d6ffSELECT value FROM config WHERE key = Ta5d6ff'Ta5d6ffc1Ta5d6ff'Ta5d6ff"Tb4b4b4)
Tff7b72assert Te6edf3row Tff7b72is Tff7b72not Tff7b72None
Tff7b72def Td2a8fftest_malicious_column_name_is_skippedTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3db_envTb4b4b4)Tb4b4b4:
T8b949e"""A column with SQL metacharacters in its name must be silently skipped."""
Te6edf3providerTb4b4b4, Te6edf3_schemaTb4b4b4, Te6edf3tmp_path Tff7b72= Te6edf3db_env
Te6edf3legacy_dir Tff7b72= Tffa657strTb4b4b4(Te6edf3tmp_path Tff7b72/ Ta5d6ff"Ta5d6fflegacy_evil_colTa5d6ff"Tb4b4b4)
Te6edf3identity_hash Tff7b72= Ta5d6ff"Ta5d6ffcc00dd00Ta5d6ff"
Te6edf3identity_dir Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3legacy_dirTb4b4b4, Ta5d6ff"Ta5d6ffidentitiesTa5d6ff"Tb4b4b4, Te6edf3identity_hashTb4b4b4)
Te6edf3osTff7b72.Td2a8ffmakedirsTb4b4b4(Te6edf3identity_dirTb4b4b4, Te6edf3exist_okTff7b72=Tff7b72TrueTb4b4b4)
Te6edf3db_path Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3identity_dirTb4b4b4, Ta5d6ff"Ta5d6ffdatabase.dbTa5d6ff"Tb4b4b4)
Te6edf3conn Tff7b72= Te6edf3sqlite3Tff7b72.Td2a8ffconnectTb4b4b4(Te6edf3db_pathTb4b4b4)
Te6edf3connTff7b72.Td2a8ffexecuteTb4b4b4(
Ta5d6ff'Ta5d6ffCREATE TABLE config (key TEXT UNIQUE, value TEXT, Ta5d6ff"Ta5d6ffkey; DROP TABLE configTa5d6ff"Ta5d6ff TEXT)Ta5d6ff'Tb4b4b4,
Tb4b4b4)
Te6edf3connTff7b72.Td2a8ffexecuteTb4b4b4(Ta5d6ff"Ta5d6ffINSERT INTO config (key, value) VALUES (Ta5d6ff'Ta5d6ffsafeTa5d6ff'Ta5d6ff, Ta5d6ff'Ta5d6ffdataTa5d6ff'Ta5d6ff)Ta5d6ff"Tb4b4b4)
Te6edf3connTff7b72.Td2a8ffcommitTb4b4b4(Tb4b4b4)
Te6edf3connTff7b72.Td2a8ffcloseTb4b4b4(Tb4b4b4)
Te6edf3migrator Tff7b72= Te6edf3LegacyMigratorTb4b4b4(Te6edf3providerTb4b4b4, Te6edf3legacy_dirTb4b4b4, Te6edf3identity_hashTb4b4b4)
Tff7b72assert Te6edf3migratorTff7b72.Td2a8ffmigrateTb4b4b4(Tb4b4b4) Tff7b72is Tff7b72True
Te6edf3row Tff7b72= Te6edf3providerTff7b72.Td2a8fffetchoneTb4b4b4(Ta5d6ff"Ta5d6ffSELECT value FROM config WHERE key = Ta5d6ff'Ta5d6ffsafeTa5d6ff'Ta5d6ff"Tb4b4b4)
Tff7b72assert Te6edf3row Tff7b72is Tff7b72not Tff7b72None
Tff7b72assert Te6edf3rowTb4b4b4[Ta5d6ff"Ta5d6ffvalueTa5d6ff"Tb4b4b4] Tff7b72== Ta5d6ff"Ta5d6ffdataTa5d6ff"
Tff7b72def Td2a8fftest_column_with_parentheses_is_skippedTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3db_envTb4b4b4)Tb4b4b4:
T8b949e"""Columns with () in the name are rejected by the identifier regex."""
Te6edf3providerTb4b4b4, Te6edf3_schemaTb4b4b4, Te6edf3tmp_path Tff7b72= Te6edf3db_env
Te6edf3legacy_dir Tff7b72= Tffa657strTb4b4b4(Te6edf3tmp_path Tff7b72/ Ta5d6ff"Ta5d6fflegacy_parens_colTa5d6ff"Tb4b4b4)
Te6edf3identity_hash Tff7b72= Ta5d6ff"Ta5d6ffee00ff00Ta5d6ff"
Te6edf3identity_dir Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3legacy_dirTb4b4b4, Ta5d6ff"Ta5d6ffidentitiesTa5d6ff"Tb4b4b4, Te6edf3identity_hashTb4b4b4)
Te6edf3osTff7b72.Td2a8ffmakedirsTb4b4b4(Te6edf3identity_dirTb4b4b4, Te6edf3exist_okTff7b72=Tff7b72TrueTb4b4b4)
Te6edf3db_path Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3identity_dirTb4b4b4, Ta5d6ff"Ta5d6ffdatabase.dbTa5d6ff"Tb4b4b4)
Te6edf3conn Tff7b72= Te6edf3sqlite3Tff7b72.Td2a8ffconnectTb4b4b4(Te6edf3db_pathTb4b4b4)
Te6edf3connTff7b72.Td2a8ffexecuteTb4b4b4(Ta5d6ff'Ta5d6ffCREATE TABLE config (key TEXT UNIQUE, value TEXT, Ta5d6ff"Ta5d6ffevil()Ta5d6ff"Ta5d6ff TEXT)Ta5d6ff'Tb4b4b4)
Te6edf3connTff7b72.Td2a8ffexecuteTb4b4b4(Ta5d6ff"Ta5d6ffINSERT INTO config (key, value) VALUES (Ta5d6ff'Ta5d6ffp1Ta5d6ff'Ta5d6ff, Ta5d6ff'Ta5d6ffparensTa5d6ff'Ta5d6ff)Ta5d6ff"Tb4b4b4)
Te6edf3connTff7b72.Td2a8ffcommitTb4b4b4(Tb4b4b4)
Te6edf3connTff7b72.Td2a8ffcloseTb4b4b4(Tb4b4b4)
Te6edf3migrator Tff7b72= Te6edf3LegacyMigratorTb4b4b4(Te6edf3providerTb4b4b4, Te6edf3legacy_dirTb4b4b4, Te6edf3identity_hashTb4b4b4)
Tff7b72assert Te6edf3migratorTff7b72.Td2a8ffmigrateTb4b4b4(Tb4b4b4) Tff7b72is Tff7b72True
Te6edf3row Tff7b72= Te6edf3providerTff7b72.Td2a8fffetchoneTb4b4b4(Ta5d6ff"Ta5d6ffSELECT value FROM config WHERE key = Ta5d6ff'Ta5d6ffp1Ta5d6ff'Ta5d6ff"Tb4b4b4)
Tff7b72assert Te6edf3row Tff7b72is Tff7b72not Tff7b72None
T8b949e# ---------------------------------------------------------------------------
T8b949e# 3. _validate_identifier — unit tests
T8b949e# ---------------------------------------------------------------------------
Tff7b72class T56d364TestValidateIdentifierTb4b4b4:
Tf0883e@pytestTff7b72.Td2a8ffmarkTff7b72.Td2a8ffparametrizeTb4b4b4(
Ta5d6ff"Ta5d6ffnameTa5d6ff"Tb4b4b4,
Tb4b4b4[
Ta5d6ff"Ta5d6ffconfigTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6fflxmf_messagesTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffATa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff_privateTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffColumn123Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffa_b_c_dTa5d6ff"Tb4b4b4,
Tb4b4b4]Tb4b4b4,
Tb4b4b4)
Tff7b72def Td2a8fftest_valid_identifiers_passTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3nameTb4b4b4)Tb4b4b4:
Tff7b72assert Te6edf3_validate_identifierTb4b4b4(Te6edf3nameTb4b4b4) Tff7b72== Te6edf3name
Tf0883e@pytestTff7b72.Td2a8ffmarkTff7b72.Td2a8ffparametrizeTb4b4b4(
Ta5d6ff"Ta5d6ffnameTa5d6ff"Tb4b4b4,
Tb4b4b4[
Ta5d6ff"Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff123abcTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6fftable nameTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffcol;dropTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffaTa5d6ff'Ta5d6ffbTa5d6ff"Tb4b4b4,
Ta5d6ff'Ta5d6ffaTa5d6ff"Ta5d6ffbTa5d6ff'Tb4b4b4,
Ta5d6ff"Ta5d6ffcol()Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffx--yTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffa,bTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffhelloTffea00\nTa5d6ffworldTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6fftabTffea00\tTa5d6ffhereTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffcol/**/nameTa5d6ff"Tb4b4b4,
Tb4b4b4]Tb4b4b4,
Tb4b4b4)
Tff7b72def Td2a8fftest_invalid_identifiers_raiseTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3nameTb4b4b4)Tb4b4b4:
Tff7b72with Te6edf3pytestTff7b72.Td2a8ffraisesTb4b4b4(Tf85149ValueErrorTb4b4b4, Te6edf3matchTff7b72=Ta5d6ff"Ta5d6ffInvalid SQLTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3_validate_identifierTb4b4b4(Te6edf3nameTb4b4b4)
T8b949e# ---------------------------------------------------------------------------
T8b949e# 4. _ensure_column — rejects injection via table/column names
T8b949e# ---------------------------------------------------------------------------
Tff7b72class T56d364TestEnsureColumnInjectionTb4b4b4:
Tff7b72def Td2a8fftest_ensure_column_rejects_malicious_table_nameTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3db_envTb4b4b4)Tb4b4b4:
Te6edf3_providerTb4b4b4, Te6edf3schemaTb4b4b4, Te6edf3_tmp_path Tff7b72= Te6edf3db_env
Tff7b72with Te6edf3pytestTff7b72.Td2a8ffraisesTb4b4b4(Tf85149ValueErrorTb4b4b4, Te6edf3matchTff7b72=Ta5d6ff"Ta5d6ffInvalid SQL table nameTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3schemaTff7b72.Td2a8ff_ensure_columnTb4b4b4(Ta5d6ff"Ta5d6ffconfig; DROP TABLE configTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffnew_colTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffTEXTTa5d6ff"Tb4b4b4)
Tff7b72def Td2a8fftest_ensure_column_rejects_malicious_column_nameTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3db_envTb4b4b4)Tb4b4b4:
Te6edf3_providerTb4b4b4, Te6edf3schemaTb4b4b4, Te6edf3_tmp_path Tff7b72= Te6edf3db_env
Tff7b72with Te6edf3pytestTff7b72.Td2a8ffraisesTb4b4b4(Tf85149ValueErrorTb4b4b4, Te6edf3matchTff7b72=Ta5d6ff"Ta5d6ffInvalid SQL column nameTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3schemaTff7b72.Td2a8ff_ensure_columnTb4b4b4(Ta5d6ff"Ta5d6ffconfigTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffcol; DROP TABLE configTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffTEXTTa5d6ff"Tb4b4b4)
Tff7b72def Td2a8fftest_ensure_column_works_for_valid_namesTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3db_envTb4b4b4)Tb4b4b4:
Te6edf3_providerTb4b4b4, Te6edf3schemaTb4b4b4, Te6edf3_tmp_path Tff7b72= Te6edf3db_env
Te6edf3result Tff7b72= Te6edf3schemaTff7b72.Td2a8ff_ensure_columnTb4b4b4(Ta5d6ff"Ta5d6ffconfigTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6fftest_new_colTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffTEXTTa5d6ff"Tb4b4b4)
Tff7b72assert Te6edf3result Tff7b72is Tff7b72True
Tff7b72def Td2a8fftest_ensure_column_idempotentTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3db_envTb4b4b4)Tb4b4b4:
Te6edf3_providerTb4b4b4, Te6edf3schemaTb4b4b4, Te6edf3_tmp_path Tff7b72= Te6edf3db_env
Te6edf3schemaTff7b72.Td2a8ff_ensure_columnTb4b4b4(Ta5d6ff"Ta5d6ffconfigTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffidempotent_colTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffTEXTTa5d6ff"Tb4b4b4)
Te6edf3result Tff7b72= Te6edf3schemaTff7b72.Td2a8ff_ensure_columnTb4b4b4(Ta5d6ff"Ta5d6ffconfigTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffidempotent_colTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffTEXTTa5d6ff"Tb4b4b4)
Tff7b72assert Te6edf3result Tff7b72is Tff7b72True
T8b949e# ---------------------------------------------------------------------------
T8b949e# 5. Property-based tests — identifier regex
T8b949e# ---------------------------------------------------------------------------
Te6edf3_IDENTIFIER_RE Tff7b72= Te6edf3reTff7b72.Td2a8ffcompileTb4b4b4(Ta5d6ffrTa5d6ff"Ta5d6ff^[A-Za-z_][A-Za-z0-9_]*$Ta5d6ff"Tb4b4b4)
Tf0883e@givenTb4b4b4(Te6edf3nameTff7b72=Te6edf3stTff7b72.Td2a8fftextTb4b4b4(Te6edf3min_sizeTff7b72=T79c0ff1Tb4b4b4, Te6edf3max_sizeTff7b72=T79c0ff80Tb4b4b4)Tb4b4b4)
Tf0883e@settingsTb4b4b4(Te6edf3deadlineTff7b72=Tff7b72NoneTb4b4b4)
Tff7b72def Td2a8fftest_validate_identifier_never_allows_sql_metacharactersTb4b4b4(Te6edf3nameTb4b4b4)Tb4b4b4:
T8b949e"""No string accepted by _validate_identifier contains SQL metacharacters."""
Tff7b72tryTb4b4b4:
Te6edf3_validate_identifierTb4b4b4(Te6edf3nameTb4b4b4)
Tff7b72except Tf85149ValueErrorTb4b4b4:
Tff7b72return
Tff7b72assert Ta5d6ff"Ta5d6ff;Ta5d6ff" Tff7b72not Tff7b72in Te6edf3name
Tff7b72assert Ta5d6ff"Ta5d6ff'Ta5d6ff" Tff7b72not Tff7b72in Te6edf3name
Tff7b72assert Ta5d6ff'Ta5d6ff"Ta5d6ff' Tff7b72not Tff7b72in Te6edf3name
Tff7b72assert Ta5d6ff"Ta5d6ff(Ta5d6ff" Tff7b72not Tff7b72in Te6edf3name
Tff7b72assert Ta5d6ff"Ta5d6ff)Ta5d6ff" Tff7b72not Tff7b72in Te6edf3name
Tff7b72assert Ta5d6ff"Ta5d6ff Ta5d6ff" Tff7b72not Tff7b72in Te6edf3name
Tff7b72assert Ta5d6ff"Ta5d6ff-Ta5d6ff" Tff7b72not Tff7b72in Te6edf3name
Tff7b72assert Ta5d6ff"Ta5d6ff/Ta5d6ff" Tff7b72not Tff7b72in Te6edf3name
Tff7b72assert Ta5d6ff"Tffea00\\Ta5d6ff" Tff7b72not Tff7b72in Te6edf3name
Tff7b72assert Ta5d6ff"Tffea00\nTa5d6ff" Tff7b72not Tff7b72in Te6edf3name
Tff7b72assert Ta5d6ff"Tffea00\rTa5d6ff" Tff7b72not Tff7b72in Te6edf3name
Tff7b72assert Ta5d6ff"Tffea00\tTa5d6ff" Tff7b72not Tff7b72in Te6edf3name
Tff7b72assert Ta5d6ff"Ta5d6ff,Ta5d6ff" Tff7b72not Tff7b72in Te6edf3name
Tff7b72assert Te6edf3_IDENTIFIER_RETff7b72.Td2a8ffmatchTb4b4b4(Te6edf3nameTb4b4b4)
Tf0883e@givenTb4b4b4(Te6edf3nameTff7b72=Te6edf3stTff7b72.Td2a8fffrom_regexTb4b4b4(Ta5d6ffrTa5d6ff"Ta5d6ff[A-Za-z_][A-Za-z0-9_]Ta5d6ff{Ta5d6ff0,30}Ta5d6ff"Tb4b4b4, Te6edf3fullmatchTff7b72=Tff7b72TrueTb4b4b4)Tb4b4b4)
Tf0883e@settingsTb4b4b4(Te6edf3deadlineTff7b72=Tff7b72NoneTb4b4b4)
Tff7b72def Td2a8fftest_validate_identifier_accepts_all_valid_identifiersTb4b4b4(Te6edf3nameTb4b4b4)Tb4b4b4:
T8b949e"""Every string matching the identifier pattern is accepted."""
Tff7b72assert Te6edf3_validate_identifierTb4b4b4(Te6edf3nameTb4b4b4) Tff7b72== Te6edf3name
Tf0883e@givenTb4b4b4(
Te6edf3nameTff7b72=Te6edf3stTff7b72.Td2a8fftextTb4b4b4(
Te6edf3alphabetTff7b72=Te6edf3stTff7b72.Td2a8ffsampled_fromTb4b4b4(Tffa657listTb4b4b4(Ta5d6ff"Ta5d6ff;Ta5d6ff'Tffea00\"Ta5d6ff()- Tffea00\tTffea00\nTffea00\rTa5d6ff,/*Ta5d6ff"Tb4b4b4)Tb4b4b4)Tb4b4b4,
Te6edf3min_sizeTff7b72=T79c0ff1Tb4b4b4,
Te6edf3max_sizeTff7b72=T79c0ff30Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tf0883e@settingsTb4b4b4(Te6edf3deadlineTff7b72=Tff7b72NoneTb4b4b4)
Tff7b72def Td2a8fftest_validate_identifier_rejects_pure_metacharacter_stringsTb4b4b4(Te6edf3nameTb4b4b4)Tb4b4b4:
T8b949e"""Strings composed entirely of SQL metacharacters are always rejected."""
Tff7b72with Te6edf3pytestTff7b72.Td2a8ffraisesTb4b4b4(Tf85149ValueErrorTb4b4b4)Tb4b4b4:
Te6edf3_validate_identifierTb4b4b4(Te6edf3nameTb4b4b4)
T8b949e# ---------------------------------------------------------------------------
T8b949e# 6. ATTACH path escaping — property-based
T8b949e# ---------------------------------------------------------------------------
Tf0883e@givenTb4b4b4(
Te6edf3path_segmentTff7b72=Te6edf3stTff7b72.Td2a8fftextTb4b4b4(
Te6edf3alphabetTff7b72=Te6edf3stTff7b72.Td2a8ffcharactersTb4b4b4(
Te6edf3whitelist_categoriesTff7b72=Tb4b4b4(Ta5d6ff"Ta5d6ffLTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffNTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffPTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffSTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffZTa5d6ff"Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Te6edf3min_sizeTff7b72=T79c0ff1Tb4b4b4,
Te6edf3max_sizeTff7b72=T79c0ff60Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tf0883e@settingsTb4b4b4(Te6edf3deadlineTff7b72=Tff7b72NoneTb4b4b4, Te6edf3suppress_health_checkTff7b72=Tb4b4b4[Te6edf3HealthCheckTff7b72.Td2a8fftoo_slowTb4b4b4]Tb4b4b4)
Tff7b72def Td2a8fftest_attach_path_escaping_never_breaks_sqlTb4b4b4(Te6edf3path_segmentTb4b4b4)Tb4b4b4:
T8b949e"""Quote-doubled ATTACH paths stay inside the SQL string literal."""
Te6edf3safe Tff7b72= Te6edf3path_segmentTff7b72.Td2a8ffreplaceTb4b4b4(Ta5d6ff"Ta5d6ff'Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff'Ta5d6ff'Ta5d6ff"Tb4b4b4)
Te6edf3sql Tff7b72= Ta5d6fffTa5d6ff"Ta5d6ffATTACH DATABASE Ta5d6ff'Tffd700{Te6edf3safeTffd700}Ta5d6ff'Ta5d6ff AS test_aliasTa5d6ff"
Tff7b72assert Te6edf3sqlTff7b72.Td2a8ffcountTb4b4b4(Ta5d6ff"Ta5d6ffATTACH DATABASE Ta5d6ff'Ta5d6ff"Tb4b4b4) Tff7b72== T79c0ff1
Te6edf3after_open Tff7b72= Te6edf3sqlTff7b72.Td2a8ffsplitTb4b4b4(Ta5d6ff"Ta5d6ffATTACH DATABASE Ta5d6ff'Ta5d6ff"Tb4b4b4, T79c0ff1Tb4b4b4)Tb4b4b4[T79c0ff1Tb4b4b4]
Te6edf3in_literal Tff7b72= Tff7b72True
Te6edf3i Tff7b72= T79c0ff0
Tff7b72while Te6edf3i Tff7b72< Tffa657lenTb4b4b4(Te6edf3after_openTb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3after_openTb4b4b4[Te6edf3iTb4b4b4] Tff7b72== Ta5d6ff"Ta5d6ff'Ta5d6ff"Tb4b4b4:
Tff7b72if Te6edf3i Tff7b72+ T79c0ff1 Tff7b72< Tffa657lenTb4b4b4(Te6edf3after_openTb4b4b4) Tff7b72and Te6edf3after_openTb4b4b4[Te6edf3i Tff7b72+ T79c0ff1Tb4b4b4] Tff7b72== Ta5d6ff"Ta5d6ff'Ta5d6ff"Tb4b4b4:
Te6edf3i Tff7b72+Tff7b72= T79c0ff2
Tff7b72continue
Te6edf3in_literal Tff7b72= Tff7b72False
Te6edf3remainder Tff7b72= Te6edf3after_openTb4b4b4[Te6edf3i Tff7b72+ T79c0ff1 Tb4b4b4:Tb4b4b4]
Tff7b72break
Te6edf3i Tff7b72+Tff7b72= T79c0ff1
Tff7b72if Tff7b72not Te6edf3in_literalTb4b4b4:
Tff7b72assert Te6edf3remainderTff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4) Tff7b72== Ta5d6ff"Ta5d6ffAS test_aliasTa5d6ff"Tb4b4b4, Tb4b4b4(
Ta5d6fffTa5d6ff"Ta5d6ffUnexpected SQL after literal end: Tffd700{Te6edf3remainderTffd700!r}Ta5d6ff"
Tb4b4b4)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────